package dev.ece.suit.loader.models.fbx;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.StringUtils;

import dev.ece.util.lib.Matrix4f;
import dev.ece.util.lib.Vector3f;

public class FBXModel {

	public final static int ROTATION_ORDER_XYZ = 0,
							ROTATION_ORDER_XZY = 1,
							ROTATION_ORDER_YZX = 2,
							ROTATION_ORDER_YXZ = 3,
							ROTATION_ORDER_ZXY = 4,
							ROTATION_ORDER_ZYX = 5;
	
	public final static String MODEL = "Model";
	
	public final static String ROOT = "Root";
	
	public final static String LIMBNODE = "LimbNode";
	
	public final static String LCL_TRANSLATION = "Lcl Translation";
	
	public final static String LCL_ROTATION = "Lcl Rotation";
	
	public final static String LCL_SCALING = "Lcl Scaling";
	
	public final static String PRE_ROTATION = "PreRotation";
	
	public final static String ROTATION_ORDER = "RotationOrder";
	
	private long id;
	
	private String name;
	
	private String type;
	
	private FBXModel parent;
	
	private FBXJoint joint;
	
	private FBXNodeAttribute nodeAttribute;
	
	private List<FBXPoseNode> poseNodes = new ArrayList<FBXPoseNode>();
	
	private FBXAnimationCurveNode translation;
	
	private FBXAnimationCurveNode rotation;
	
	private FBXAnimationCurveNode scaling;
	
	private Vector3f lcltranslation = new Vector3f();
	
	private Vector3f lclrotation = new Vector3f();
	
	private Vector3f prerotation = new Vector3f();
	
	private Vector3f lclscaling = new Vector3f(1, 1, 1);
	
	private int rotationOrder;
	
	private Matrix4f globalMatrix = new Matrix4f();
	
	public static FBXModel[] parseFBXModel(List<FBXElement> elements) {
		List<FBXModel> models = new ArrayList<FBXModel>();
		FBXModel model = null;
		String type = null;
		for(FBXElement element: elements) {
			type = (String) element.getProperties()[2].getValue();
			model = StringUtils.equals(FBXModelLoader.MESH, type) ? new FBXMeshModel() : new FBXModel();
			model.type = type;
			model.fill(element);
			models.add(model);
		}
		return models.isEmpty() ? null: models.toArray(new FBXModel[] {});
	}
	
	protected void fill(FBXElement element) {
		this.id = (Long) element.getProperties()[0].getValue();
		this.name = (String) element.getProperties()[1].getValue();
		Vector3f rotation = null;
		String type = null;
		FBXElement properties = element.findFirstChild(FBXModelLoader.PROPERTIES70);
		for(FBXElement child: properties.getChildren()) {
			if(StringUtils.equals(FBXModelLoader.P, child.getId())) {
				if(child.hasProperties()) {
					type = (String) child.getProperties()[0].getValue();
					if(StringUtils.equals(ROTATION_ORDER, type)) {
						rotationOrder = (Integer) child.getProperties()[4].getValue();
					} else if(StringUtils.equals(LCL_TRANSLATION, type)) {
						lcltranslation.set(toVector3f(child));
					} else if(StringUtils.equals(LCL_ROTATION, type)) {
						rotation = toVector3f(child);
						lclrotation.set(
								(float) Math.toRadians(rotation.getX()),
								(float) Math.toRadians(rotation.getY()),
								(float) Math.toRadians(rotation.getZ()));
					} else if(StringUtils.equals(LCL_SCALING, type)) {
						lclscaling.set(toVector3f(child));
					} else if(StringUtils.equals(PRE_ROTATION, type)) {
						rotation = toVector3f(child);
						prerotation.set(
								(float) Math.toRadians(rotation.getX()),
								(float) Math.toRadians(rotation.getY()),
								(float) Math.toRadians(rotation.getZ()));
					}
					onChildProperties(type, child);
				}
			}
		}
	}
	
	protected void onChildProperties(String type, FBXElement child) {}
	
	public static Vector3f toVector3f(FBXElement element) {
		return new Vector3f(
				((Double) element.getProperties()[4].getValue()).floatValue(),
				((Double) element.getProperties()[5].getValue()).floatValue(),
				((Double) element.getProperties()[6].getValue()).floatValue()
		);
	}
	
	public static Matrix4f rotation(Vector3f euler, int rotationOrder) {
		Matrix4f matrix = new Matrix4f();
		matrix.identity();
		Matrix4f rx = rotationX(euler.getX());
		Matrix4f ry = rotationY(euler.getY());
		Matrix4f rz = rotationZ(euler.getZ());
		switch(rotationOrder) {
			case ROTATION_ORDER_XZY:
				matrix.mul(ry, rz);
				matrix.mul(rx);
				break;
			case ROTATION_ORDER_YZX:
				matrix.mul(rx, rz);
				matrix.mul(ry);
				break;
			case ROTATION_ORDER_YXZ:
				matrix.mul(rz, rx);
				matrix.mul(ry);
				break;
			case ROTATION_ORDER_ZXY:
				matrix.mul(ry, rx);
				matrix.mul(rz);
				break;
			case ROTATION_ORDER_ZYX:
				matrix.mul(rx, ry);
				matrix.mul(rz);
				break;
			case ROTATION_ORDER_XYZ:
			default:
				matrix.mul(rz, ry);
				matrix.mul(rx);
		}
		return matrix;
	}
	
	public static Matrix4f rotationX(float angle) {
		Matrix4f matrix = new Matrix4f();
		matrix.identity();
		float c = (float) Math.cos(angle);
		float s = (float) Math.sin(angle);
		float[] m = matrix.getMatrix();
		m[5] = m[10] = c;
		m[9] = -s;
		m[6] = s;
		return matrix;
	}
	
	public static Matrix4f rotationY(float angle) {
		Matrix4f matrix = new Matrix4f();
		matrix.identity();
		float c = (float) Math.cos(angle);
		float s = (float) Math.sin(angle);
		float[] m = matrix.getMatrix();
		m[0] = m[10] = c;
		m[8] = s;
		m[2] = -s;
		return matrix;
	}
	
	public static Matrix4f rotationZ(float angle) {
		Matrix4f matrix = new Matrix4f();
		matrix.identity();
		float c = (float) Math.cos(angle);
		float s = (float) Math.sin(angle);
		float[] m = matrix.getMatrix();
		m[0] = m[5] = c;
		m[4] = -s;
		m[1] = s;
		return matrix;
	}
	
	public static void connectToFBXJoint(Map<Long, List<FBXConnection>> connections, FBXModel[] models, FBXJoint[] joints) {
		Map<Long, FBXJoint> jointmap = FBXJoint.toFBXJointMap(joints);
		if(models != null) {
			Map<Long, FBXModel> modelmap = FBXModel.toFBXModelMap(models);
			List<FBXConnection> conns = null;
			FBXJoint joint = null;
			FBXModel parent = null;
			for(FBXModel model: models) {
				conns = connections.get(model.getId());
				if(conns != null) {
					for(FBXConnection conn: conns) {
						joint = jointmap.get(conn.getTo());
						if(joint != null) {
							model.joint = joint;
							joint.setModel(model);
						}
						parent = modelmap.get(conn.getTo());
						if(parent != null) {
							model.parent = parent;
						}
					}
				}
			}
		}
	}
	
	public static Map<Long, FBXModel> toFBXModelMap(FBXModel[] models) {
		Map<Long, FBXModel> map = new ConcurrentHashMap<Long, FBXModel>();
		if(models != null) {
			for(FBXModel model: models) {
				map.put(model.getId(), model);
			}
		}
		return map;
	}
	
	public static void setup(FBXModel[] models) {
		if(models != null && models.length > 0) {
			for(FBXModel model: models) {
				model.setup();
			}
		}
	}
	
	protected void setup() {
		Matrix4f t = new Matrix4f();
		t.identity();
		t.translate(this.getLcltranslation().getX(), getLcltranslation().getY(), getLcltranslation().getZ());
		Matrix4f pre = rotation(this.getPrerotation(), ROTATION_ORDER_XYZ);
		Matrix4f r = rotation(this.getLclrotation(), getRotationOrder());
		Matrix4f s = new Matrix4f();
		s.identity();
		s.scale(this.getLclscaling());
		Matrix4f matrix = this.getGlobalMatrix();
		matrix.mul(t, pre);
		matrix.mul(r);
		matrix.mul(s);
		if(this.hasParent()) {
			matrix.mul(getParent().getGlobalMatrix(), matrix);
		}
	}
	
	public void updateGlobalTransform(long current) {
//		Matrix4f local = getLocalTransform(current);
//		if(this.hasParent()) {
//			getGlobalMatrix().mul(getParent().getGlobalMatrix(), local);
//		} else {
//			getGlobalMatrix().set(local);
//		}
	}
	
	public Matrix4f getLocalTransform(long fbx_time) {
		Vector3f position = this.translation != null ? this.translation.toLoalTransform(fbx_time) : this.getLcltranslation();
		Vector3f euler = this.rotation != null ? this.rotation.toLoalTransform(fbx_time) : null;
		Vector3f scale = this.scaling != null ? this.scaling.toLoalTransform(fbx_time) : this.getLclscaling();
		Vector3f rotation = euler != null ? new Vector3f(
				(float) Math.toRadians(euler.getX()),
				(float) Math.toRadians(euler.getY()),
				(float) Math.toRadians(euler.getZ())) : this.getLclrotation();
		Matrix4f t = new Matrix4f();
		t.identity();
		t.translate(position);
		Matrix4f pre = rotation(this.getPrerotation(), ROTATION_ORDER_XYZ);
		Matrix4f r = rotation(rotation, this.getRotationOrder());
		Matrix4f s = new Matrix4f();
		s.identity();
		s.scale(scale);
		
		Matrix4f matrix = new Matrix4f();
		matrix.identity();
		matrix.mul(t, pre);
		matrix.mul(r);
		matrix.mul(s);
		return matrix;
	}
	
	public FBXAnimationCurveNode getTranslation() {
		return translation;
	}

	public void setTranslation(FBXAnimationCurveNode translation) {
		this.translation = translation;
	}

	public FBXAnimationCurveNode getRotation() {
		return rotation;
	}

	public void setRotation(FBXAnimationCurveNode rotation) {
		this.rotation = rotation;
	}

	public FBXAnimationCurveNode getScaling() {
		return scaling;
	}

	public void setScaling(FBXAnimationCurveNode scaling) {
		this.scaling = scaling;
	}

	public FBXNodeAttribute getNodeAttribute() {
		return nodeAttribute;
	}

	protected void setNodeAttribute(FBXNodeAttribute nodeAttribute) {
		this.nodeAttribute = nodeAttribute;
	}

	public Vector3f getLcltranslation() {
		return lcltranslation;
	}

	public Vector3f getLclrotation() {
		return lclrotation;
	}

	public Vector3f getLclscaling() {
		return lclscaling;
	}

	public Vector3f getPrerotation() {
		return prerotation;
	}

	public boolean hasParent() {
		return parent != null;
	}
	
	public long getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public String getType() {
		return type;
	}

	public FBXModel getParent() {
		return parent;
	}

	public FBXJoint getJoint() {
		return joint;
	}

	public List<FBXPoseNode> getPoseNodes() {
		return poseNodes;
	}

	public int getRotationOrder() {
		return rotationOrder;
	}

	public Matrix4f getGlobalMatrix() {
		return globalMatrix;
	}
	
}
